agora inbox for [email protected]
help / color / mirror / Atom feedImprove Assert output
1738+ messages / 3 participants
[nested] [flat]
* Improve Assert output
@ 2019-08-14 20:28 Peter Eisentraut <[email protected]>
0 siblings, 1 reply; 1738+ messages in thread
From: Peter Eisentraut @ 2019-08-14 20:28 UTC (permalink / raw)
To: pgsql-hackers
This kind of output is usually not helpful:
TRAP: BadArgument("((context) != ((void *)0) && (((((const
Node*)((context)))->type) == T_AllocSetContext) || ((((const
Node*)((context)))->type) == T_SlabContext) || ((((const
Node*)((context)))->type) == T_GenerationContext)))", File:
"../../../../src/include/utils/memutils.h", Line: 129)
What we probably want is something like:
TRAP: BadArgument("MemoryContextIsValid(context)", File:
"../../../../src/include/utils/memutils.h", Line: 129)
The problem is that the way the Assert macros are written they
macro-expand the arguments before stringifying them. The attached patch
fixes that. This requires both replacing CppAsString by plain "#" and
not redirecting Assert() to Trap().
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
From 7db60e612e1d26c7a1859948594c28dc3b54353e Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Wed, 14 Aug 2019 22:23:34 +0200
Subject: [PATCH] Improve Assert output
If an assertion expression contained a macro, the failed assertion
message would print the expanded macro, which is usually unhelpful and
confusing. Restructure the Assert macros to not expand any macros
when constructing the failure message.
---
src/include/c.h | 26 ++++++++++++++++++++------
1 file changed, 20 insertions(+), 6 deletions(-)
diff --git a/src/include/c.h b/src/include/c.h
index 2a082afab1..f461628a24 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -755,7 +755,7 @@ typedef NameData *Name;
#define Trap(condition, errorType) \
do { \
if (condition) \
- ExceptionalCondition(CppAsString(condition), (errorType), \
+ ExceptionalCondition(#condition, (errorType), \
__FILE__, __LINE__); \
} while (0)
@@ -768,20 +768,34 @@ typedef NameData *Name;
*/
#define TrapMacro(condition, errorType) \
((bool) (! (condition) || \
- (ExceptionalCondition(CppAsString(condition), (errorType), \
+ (ExceptionalCondition(#condition, (errorType), \
__FILE__, __LINE__), 0)))
#define Assert(condition) \
- Trap(!(condition), "FailedAssertion")
+ do { \
+ if (!(condition)) \
+ ExceptionalCondition(#condition, "FailedAssertion", \
+ __FILE__, __LINE__); \
+ } while (0)
#define AssertMacro(condition) \
- ((void) TrapMacro(!(condition), "FailedAssertion"))
+ ((void) ((condition) || \
+ (ExceptionalCondition(#condition, "FailedAssertion", \
+ __FILE__, __LINE__), 0)))
#define AssertArg(condition) \
- Trap(!(condition), "BadArgument")
+ do { \
+ if (!(condition)) \
+ ExceptionalCondition(#condition, "BadArgument", \
+ __FILE__, __LINE__); \
+ } while (0)
#define AssertState(condition) \
- Trap(!(condition), "BadState")
+ do { \
+ if (!(condition)) \
+ ExceptionalCondition(#condition, "BadState", \
+ __FILE__, __LINE__); \
+ } while (0)
/*
* Check that `ptr' is `bndr' aligned.
--
2.22.0
Attachments:
[text/plain] 0001-Improve-Assert-output.patch (2.1K, ../../[email protected]/2-0001-Improve-Assert-output.patch)
download | inline diff:
From 7db60e612e1d26c7a1859948594c28dc3b54353e Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <[email protected]>
Date: Wed, 14 Aug 2019 22:23:34 +0200
Subject: [PATCH] Improve Assert output
If an assertion expression contained a macro, the failed assertion
message would print the expanded macro, which is usually unhelpful and
confusing. Restructure the Assert macros to not expand any macros
when constructing the failure message.
---
src/include/c.h | 26 ++++++++++++++++++++------
1 file changed, 20 insertions(+), 6 deletions(-)
diff --git a/src/include/c.h b/src/include/c.h
index 2a082afab1..f461628a24 100644
--- a/src/include/c.h
+++ b/src/include/c.h
@@ -755,7 +755,7 @@ typedef NameData *Name;
#define Trap(condition, errorType) \
do { \
if (condition) \
- ExceptionalCondition(CppAsString(condition), (errorType), \
+ ExceptionalCondition(#condition, (errorType), \
__FILE__, __LINE__); \
} while (0)
@@ -768,20 +768,34 @@ typedef NameData *Name;
*/
#define TrapMacro(condition, errorType) \
((bool) (! (condition) || \
- (ExceptionalCondition(CppAsString(condition), (errorType), \
+ (ExceptionalCondition(#condition, (errorType), \
__FILE__, __LINE__), 0)))
#define Assert(condition) \
- Trap(!(condition), "FailedAssertion")
+ do { \
+ if (!(condition)) \
+ ExceptionalCondition(#condition, "FailedAssertion", \
+ __FILE__, __LINE__); \
+ } while (0)
#define AssertMacro(condition) \
- ((void) TrapMacro(!(condition), "FailedAssertion"))
+ ((void) ((condition) || \
+ (ExceptionalCondition(#condition, "FailedAssertion", \
+ __FILE__, __LINE__), 0)))
#define AssertArg(condition) \
- Trap(!(condition), "BadArgument")
+ do { \
+ if (!(condition)) \
+ ExceptionalCondition(#condition, "BadArgument", \
+ __FILE__, __LINE__); \
+ } while (0)
#define AssertState(condition) \
- Trap(!(condition), "BadState")
+ do { \
+ if (!(condition)) \
+ ExceptionalCondition(#condition, "BadState", \
+ __FILE__, __LINE__); \
+ } while (0)
/*
* Check that `ptr' is `bndr' aligned.
--
2.22.0
^ permalink raw reply [nested|flat] 1738+ messages in thread
* Re: Improve Assert output
@ 2019-08-14 20:36 Tom Lane <[email protected]>
parent: Peter Eisentraut <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Tom Lane @ 2019-08-14 20:36 UTC (permalink / raw)
To: Peter Eisentraut <[email protected]>; +Cc: pgsql-hackers
Peter Eisentraut <[email protected]> writes:
> This kind of output is usually not helpful:
> TRAP: BadArgument("((context) != ((void *)0) && (((((const
> Node*)((context)))->type) == T_AllocSetContext) || ((((const
> Node*)((context)))->type) == T_SlabContext) || ((((const
> Node*)((context)))->type) == T_GenerationContext)))", File:
> "../../../../src/include/utils/memutils.h", Line: 129)
> What we probably want is something like:
> TRAP: BadArgument("MemoryContextIsValid(context)", File:
> "../../../../src/include/utils/memutils.h", Line: 129)
+1, that would be a big improvement. The other thing that this
is fixing is that the existing output for Assert et al shows
the *inverted* condition, which I for one always found confusing.
I didn't try to test the patch, but it passes eyeball examination.
regards, tom lane
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
end of thread, other threads:[~2026-04-27 14:23 UTC | newest]
Thread overview: 1738+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2019-08-14 20:28 Improve Assert output Peter Eisentraut <[email protected]>
2019-08-14 20:36 ` Tom Lane <[email protected]>
2026-04-27 14:23 [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 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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 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 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 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 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 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 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 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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 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 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 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 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 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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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 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 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 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 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 v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [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]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox